﻿''' <summary>
''' Manage the downloading of updates from web site
''' </summary>
''' <remarks>
''' The version number will be taken from the installer file name which must conform to this pattern:
''' Setup {basename} m_n_b_r.exe
''' where m is major version, n is minor, b is build, r is revision; these must be explicitly set as Assembly information
''' and the setup file must be given the matching name
''' 
''' Because some organizations prohibit transfer of .exe files, also check for .exe_rename files that can be downloaded, 
''' renamed, and run.
''' </remarks>
Public Class clsUpdates
    Private _BaseName As String
    Private _WebsiteURL As String

    Sub New(ByVal BaseName As String, ByVal WebsiteURL As String)
        _BaseName = BaseName
        _WebsiteURL = WebsiteURL
    End Sub

    Friend Event Progress(ByVal Value As Integer, ByVal MaxValue As Integer, ByVal Text As String)
    Friend Event ProgressComplete()

    ''' <summary>
    ''' This will get a list of available files at the specified UpdateLocation (either web site or network directory)
    ''' Then the assembly version will be compared to the latest version and an update will occur if necessary
    ''' If return True, caller should retrieve value of ShutDownCommandline, run it, and them immediately close the application
    ''' </summary>
    ''' <param name="ShowIsUpdatedMsg">If true, will display message that current version is up to date if it is</param>
    Friend Function UpdatesInstalled(ByVal UpdateLocation As String, ByVal ShowIsUpdatedMsg As Boolean, ByRef ShutdownCommandline As String) As Boolean
        Try
            ShutdownCommandline = ""
            RaiseEvent Progress(0, 100, "Checking for updates...")

            Dim lstFiles As New Generic.List(Of String)

            'this routine just downloads web page and parses it using regular expressions
            'a more robust approach would use a full-fledged HTML parser, but that is overkill for this
            'the regex expression was acquired on internet and appears to work ok

            If UpdateLocation.Contains("/") Then 'source path is URL
                Dim URL As String = UpdateLocation
                If Not URL.ToUpper.StartsWith("HTTP://") Then URL = "http://" & URL
                If Not URL.EndsWith("/") Then URL &= "/"

                Dim request As Net.HttpWebRequest
                request = Net.HttpWebRequest.Create(UpdateLocation)
                With request
                    Dim response As Net.HttpWebResponse = .GetResponse
                    Dim responseStream As IO.Stream = response.GetResponseStream
                    Dim sr As New IO.StreamReader(responseStream)
                    Dim html As String = sr.ReadToEnd
                    Dim regexp As String = "<a.*?href=[""'](?<url>.*?)[""'].*?>(?<name>.*?)</a>"
                    Dim regex As New System.Text.RegularExpressions.Regex(regexp, System.Text.RegularExpressions.RegexOptions.IgnoreCase)
                    Dim matches As System.Text.RegularExpressions.MatchCollection = regex.Matches(html)
                    For Each match As System.Text.RegularExpressions.Match In matches
                        If match.Success Then lstFiles.Add(match.Groups("name").Value)
                    Next
                    sr.Close()
                    sr.Dispose()
                    responseStream.Dispose()
                End With
            Else 'is local or network folder
                Dim URL As String = UpdateLocation
                If Not URL.EndsWith("\") Then URL &= "\"
                If Not My.Computer.FileSystem.DirectoryExists(URL) Then
                    RaiseEvent ProgressComplete()
                    MessageBox.Show(String.Format("The update location you specified ({0}) does not exist. Please check your project preferences.", URL), "Program Update", MessageBoxButtons.OK, MessageBoxIcon.Warning)
                    Return False
                End If
                For Each fn As String In My.Computer.FileSystem.GetFiles(URL, FileIO.SearchOption.SearchTopLevelOnly, String.Format("Setup {0} *.exe*", _BaseName))
                    lstFiles.Add(IO.Path.GetFileName(fn))
                Next
            End If

            'this will get the version of the entire application; however, this DLL may get loaded from another application, so get that version instead

            Dim MyVersion As System.Version = Reflection.Assembly.GetExecutingAssembly.GetName.Version

            'determine the latest version from all the files in the list (*** but do not allow auto-upgrade to next major version number (going from 5.0 to 5.1 is a major version upgrade) ***)

            Dim MaxVersion As New System.Version(0, 0, 0, 0)
            Dim MaxMajorVersion As New System.Version(0, 0, 0, 0)
            Dim MajorUpgradeAvailable As Boolean = False
            Dim InstallFile As String = ""
            'parse the filename; first look for .exe_rename files, then .exe files..._rename files with same version number will take precidence
            For i As Integer = 1 To 2
                For Each fn As String In lstFiles
                    If fn Like String.Format("Setup {0} *.exe{1}", _BaseName, Choose(i, "_rename", "")) Then
                        Dim sVer As String = fn.Split(".")(0).Split(" ")(2)
                        Dim ar() As String = sVer.Split("_")

                        'ignore misformatted filenames
                        Dim NonNumeric As Boolean = False
                        For Each s As String In ar
                            If Not IsNumeric(s) Then NonNumeric = True
                        Next
                        If NonNumeric Then Continue For

                        Dim ThisVersion As System.Version
                        Select Case ar.Length
                            Case 1 : ThisVersion = New Version(ar(0), 0)
                            Case 2 : ThisVersion = New Version(ar(0), ar(1))
                            Case 3 : ThisVersion = New Version(ar(0), ar(1), ar(2))
                            Case 4 : ThisVersion = New Version(ar(0), ar(1), ar(2), ar(3))
                            Case Else : ThisVersion = New Version(0)
                        End Select
                        If ThisVersion.CompareTo(MaxVersion) > 0 And ThisVersion.ToString(2) = MyVersion.ToString(2) Then
                            MaxVersion = ThisVersion
                            InstallFile = fn
                        End If
                        If ThisVersion.CompareTo(MaxMajorVersion) > 0 Then
                            If ThisVersion.ToString(2) > MyVersion.ToString(2) Then
                                MaxMajorVersion = ThisVersion
                                MajorUpgradeAvailable = True
                            End If
                        End If
                    End If
                Next
            Next

            If MaxVersion.CompareTo(MyVersion) > 0 Then 'is newer; must install

                Dim msg As String = String.Format("A new version of {0} is available (Version {1}). Do you want to download and install the update ({0} will automatically shut down and restart, however you must be sure that all applications that reference the libraries associated with {0} are also shut down)?{2}{2}You can configure the program to turn off automatic updates.", _BaseName, MaxVersion, vbCr)

                Dim InstallDir As String = My.Application.Info.DirectoryPath

                RaiseEvent ProgressComplete()

                If MessageBox.Show(msg, "Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1) = DialogResult.OK Then
                    Dim downloadOK As Boolean = True
                    Dim scriptfile As String = "", batchfile As String = ""
                    scriptfile = My.Computer.FileSystem.SpecialDirectories.Temp & "\Install.vbs"
                    Dim sw As New IO.StreamWriter(scriptfile, False)
                    sw.WriteLine("set shell=createobject(""wscript.shell"")")

                    'store installers in central location so can roll back to earlier version

                    Dim SetupDir As String = InstallDir & "\Setup Files"
                    If Not My.Computer.FileSystem.DirectoryExists(SetupDir) Then My.Computer.FileSystem.CreateDirectory(SetupDir)
                    Dim NumSetup As Integer = My.Computer.FileSystem.GetFiles(SetupDir, FileIO.SearchOption.SearchTopLevelOnly, String.Format("Setup {0}*.exe", _BaseName)).Count
                    'Dim Bypass As Boolean = GetSetting(REGAPP, "UpdateWarning", "Bypass", False)
                    'If Not Bypass And NumSetup > 5 Then
                    '    Select Case MessageBox.Show(String.Format("There are {0} archived setup files stored at {1}. Do you want to browse to this location so you can delete some of the older versions to reduce disk space usage?\n\nSelect 'Yes' to open the browser, 'No' to skip, 'Cancel' to suppress this message in the future.\n\n(Note that these setup files are not removed when you uninstall the program.)".Replace("\n", vbCr), NumSetup, SetupDir), "Update Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning)
                    '        Case DialogResult.Yes : Process.Start(SetupDir)
                    '        Case DialogResult.No
                    '        Case DialogResult.Cancel : SaveSetting(REGAPP, "UpdateWarning", "Bypass", True)
                    '    End Select
                    'End If

                    If DownloadFile(UpdateLocation, SetupDir, InstallFile) Then
                        'file is automatically renamed after download
                        sw.WriteLine("shell.run '''{0}\{1}'' /SP /SILENT /NOICONS /TYPE=update /DIR=''{2}''',1,true".Replace("'", """"), SetupDir, InstallFile.Replace("_rename", ""), InstallDir)
                    Else
                        downloadOK = False
                    End If
                    sw.WriteLine("shell.run '''{0}\{1}.exe'''".Replace("'", """"), InstallDir, _BaseName)
                    sw.Close()
                    sw.Dispose()

                    If downloadOK Then
                        If Debugger.IsAttached Then
                            MessageBox.Show("Because debugger is being used, will not install updates. Script file is: " & scriptfile)
                            Return False
                        Else
                            'need to delay start of scriptfile until program is just about shutdown
                            ShutdownCommandline = """" & scriptfile & """"
                            Return True
                        End If
                    Else
                        If MessageBox.Show(String.Format("Unable to download new setup file from Update Location specified in program preferences ({0}); do you want to download and install the updates manually?", UpdateLocation), "Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) = DialogResult.OK Then
                            Process.Start(_WebsiteURL)
                        End If
                    End If
                End If
            ElseIf ShowIsUpdatedMsg Then
                RaiseEvent ProgressComplete()
                Application.DoEvents()
                MessageBox.Show(String.Format("No updates are available for this version of {0}; your version ({1}) is up-to-date!", _BaseName, MyVersion), "Check for Updates", MessageBoxButtons.OK, MessageBoxIcon.Information)
            End If

            If MajorUpgradeAvailable Then
                Dim IgnoreThisUpgrade As Boolean = TestNull(GetSetting(REGAPP, "IgnoreUpgrade", MaxMajorVersion.ToString(2)), False)
                If Not IgnoreThisUpgrade Then
                    Select Case MessageBox.Show(String.Format("Attention: a major upgrade for {0} is available (Version {1}) but cannot be automatically updated. Do you want to download and install the new version manually (if you have not already done so)? Note that the new and current versions will coexist on your computer.{2}{2}  - Click Yes to open Internet browser to application download page{2}  - Click No to continue without updating{2}  - Click Cancel to stop being reminded about this new major version", _BaseName, MaxMajorVersion.ToString(2), vbCr), "Notice", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Asterisk)
                        Case DialogResult.Yes : Process.Start(_WebsiteURL)
                        Case DialogResult.No
                        Case DialogResult.Cancel : SaveSetting(REGAPP, "IgnoreUpgrade", MaxMajorVersion.ToString(2), "true")
                    End Select
                End If
            End If

            Return False
        Catch ex As Exception
            'ErrorMsg(, ex)
            If MessageBox.Show(String.Format("Unable to download new setup file from Update Location specified in program preferences ({0}); do you want to download and install the updates manually?{1}{1}(The detailed error message was: {2}){1}{1}If this message reoccurs, you can disable automatic checking for updates under program preferences.", UpdateLocation, vbCr, ex.Message), "Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) = DialogResult.OK Then
                Process.Start(_WebsiteURL)
            End If
        Finally
            RaiseEvent ProgressComplete()
        End Try
    End Function

    ''' <summary>
    ''' Download file or web page from the Internet
    ''' </summary>
    ''' <param name="SourcePath">URL of remote web page or file to be downloaded or path containing file</param>
    ''' <param name="DestPath">Path where file should be placed</param>
    ''' <param name="FileName">Local name of file to be copied (note: if filename ends with _rename that will be stripped when saved)</param>
    Friend Function DownloadFile(ByVal SourcePath As String, ByVal DestPath As String, ByVal FileName As String) As Boolean
        RaiseEvent Progress(0, 100, String.Format("Downloading {0} from {1}", FileName, SourcePath))

        Dim fs As IO.FileStream = Nothing
        Dim rs As IO.Stream = Nothing
        Try
            If Not DestPath.EndsWith("\") Then DestPath &= "\"

            If SourcePath.Contains("/") Then 'source path is URL
                Dim URL As String = SourcePath
                If Not URL.ToUpper.StartsWith("HTTP://") Then URL = "http://" & URL
                If Not URL.EndsWith("/") Then URL &= "/"

                'get size of file to download
                Dim req As Net.HttpWebRequest = Net.HttpWebRequest.Create(URL & FileName)

                req.Proxy = Net.WebRequest.DefaultWebProxy
                req.Proxy.Credentials = System.Net.CredentialCache.DefaultCredentials

                req.Method = "HEAD"
                req.Timeout = 15000
                Dim res As Net.WebResponse = req.GetResponse
                Dim FileSize As Long = 0
                Long.TryParse(res.Headers.Get("Content-Length"), FileSize)
                res.Close()

                Dim objReq As Net.HttpWebRequest = Net.WebRequest.Create(New Uri(URL & FileName))
                With objReq
                    .Timeout = 15000 '15 second timeout

                    Dim resp As Net.WebResponse = Nothing
                    Try
                        resp = .GetResponse
                    Catch ex As Exception
                        Return False
                    End Try

                    fs = New IO.FileStream(DestPath & FileName.Replace("_rename", ""), IO.FileMode.Create)
                    rs = resp.GetResponseStream()

                    ' Create a local buffer for a temporary storage of the read data, 100k in size
                    Dim readBuffer(1024 * 100 - 1) As Byte

                    ' Read the first 100 KB.
                    Dim count As Integer = rs.Read(readBuffer, 0, readBuffer.Length)
                    Dim totalcount As Integer = count

                    'Loop to read the remaining data in blocks of 100 KB
                    While count > 0
                        fs.Write(readBuffer, 0, count)
                        count = rs.Read(readBuffer, 0, readBuffer.Length)
                        totalcount += count
                        If FileSize = 0 Then
                            Application.DoEvents()
                        Else
                            RaiseEvent Progress(totalcount, FileSize, String.Format("Downloading ""{0}"" from ""{1}""", FileName, SourcePath))
                        End If
                    End While
                End With
            Else
                If Not SourcePath.EndsWith("\") Then SourcePath &= "\"
                If Not (My.Computer.FileSystem.DirectoryExists(SourcePath) AndAlso
                        My.Computer.FileSystem.FileExists(SourcePath & FileName)) Then Return False
                My.Computer.FileSystem.CopyFile(SourcePath & FileName, DestPath & FileName.Replace("_rename", ""), True)
            End If
            Return True
        Catch ex As Exception
            Return False
        Finally
            If fs IsNot Nothing Then
                fs.Close()
                fs.Dispose()
            End If
            If rs IsNot Nothing Then
                rs.Close()
                rs.Dispose()
            End If
            RaiseEvent ProgressComplete()
            Application.DoEvents()
        End Try
    End Function

End Class
